/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @flow
 */
'use strict'

var Animation = require('./Animation')
var AnimatedValue = require('./AnimatedValue')
var RequestAnimationFrame = require('./injectable/RequestAnimationFrame')
var CancelAnimationFrame = require('./injectable/CancelAnimationFrame')
var invariant = require('invariant')
var SpringConfig = require('./SpringConfig')

import type { AnimationConfig, EndCallback } from './Animation'

type SpringAnimationConfigSingle = AnimationConfig & {
  toValue: number | AnimatedValue;
  overshootClamping?: bool;
  restDisplacementThreshold?: number;
  restSpeedThreshold?: number;
  velocity?: number;
  bounciness?: number;
  speed?: number;
  tension?: number;
  friction?: number;
};

function withDefault<T>(value: ?T, defaultValue: T): T {
  if (value === undefined || value === null) {
    return defaultValue
  }
  return value
}

class SpringAnimation extends Animation {
  _overshootClamping: bool;
  _restDisplacementThreshold: number;
  _restSpeedThreshold: number;
  _initialVelocity: ?number;
  _lastVelocity: number;
  _startPosition: number;
  _lastPosition: number;
  _fromValue: number;
  _toValue: any;
  _tension: number;
  _friction: number;
  _lastTime: number;
  _onUpdate: (value: number) => void;
  _animationFrame: any;

  constructor(
    config: SpringAnimationConfigSingle,
  ) {
    super()

    this._overshootClamping = withDefault(config.overshootClamping, false)
    this._restDisplacementThreshold = withDefault(config.restDisplacementThreshold, 0.001)
    this._restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001)
    this._initialVelocity = config.velocity
    this._lastVelocity = withDefault(config.velocity, 0)
    this._toValue = config.toValue
    this.__isInteraction = config.isInteraction !== undefined ? config.isInteraction : true

    var springConfig
    if (config.bounciness !== undefined || config.speed !== undefined) {
      invariant(
        config.tension === undefined && config.friction === undefined,
        'You can only define bounciness/speed or tension/friction but not both',
      )
      springConfig = SpringConfig.fromBouncinessAndSpeed(
        withDefault(config.bounciness, 8),
        withDefault(config.speed, 12),
      )
    } else {
      springConfig = SpringConfig.fromOrigamiTensionAndFriction(
        withDefault(config.tension, 40),
        withDefault(config.friction, 7),
      )
    }
    this._tension = springConfig.tension
    this._friction = springConfig.friction
  }

  start(
    fromValue: number,
    onUpdate: (value: number) => void,
    onEnd: ?EndCallback,
    previousAnimation: ?Animation,
  ): void {
    this.__active = true
    this._startPosition = fromValue
    this._lastPosition = this._startPosition

    this._onUpdate = onUpdate
    this.__onEnd = onEnd
    this._lastTime = Date.now()

    if (previousAnimation instanceof SpringAnimation) {
      var internalState = previousAnimation.getInternalState()
      this._lastPosition = internalState.lastPosition
      this._lastVelocity = internalState.lastVelocity
      this._lastTime = internalState.lastTime
    }
    if (this._initialVelocity !== undefined &&
        this._initialVelocity !== null) {
      this._lastVelocity = this._initialVelocity
    }
    this.onUpdate()
  }

  getInternalState(): Object {
    return {
      lastPosition: this._lastPosition,
      lastVelocity: this._lastVelocity,
      lastTime: this._lastTime
    }
  }

  onUpdate(): void {
    var position = this._lastPosition
    var velocity = this._lastVelocity

    var tempPosition = this._lastPosition
    var tempVelocity = this._lastVelocity

    // If for some reason we lost a lot of frames (e.g. process large payload or
    // stopped in the debugger), we only advance by 4 frames worth of
    // computation and will continue on the next frame. It's better to have it
    // running at faster speed than jumping to the end.
    var MAX_STEPS = 64
    var now = Date.now()
    if (now > this._lastTime + MAX_STEPS) {
      now = this._lastTime + MAX_STEPS
    }

    // We are using a fixed time step and a maximum number of iterations.
    // The following post provides a lot of thoughts into how to build this
    // loop: http://gafferongames.com/game-physics/fix-your-timestep/
    var TIMESTEP_MSEC = 1
    var numSteps = Math.floor((now - this._lastTime) / TIMESTEP_MSEC)

    for (var i = 0; i < numSteps; ++i) {
      // Velocity is based on seconds instead of milliseconds
      var step = TIMESTEP_MSEC / 1000

      // This is using RK4. A good blog post to understand how it works:
      // http://gafferongames.com/game-physics/integration-basics/
      var aVelocity = velocity
      var aAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity
      var tempPosition = position + aVelocity * step / 2
      var tempVelocity = velocity + aAcceleration * step / 2

      var bVelocity = tempVelocity
      var bAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity
      tempPosition = position + bVelocity * step / 2
      tempVelocity = velocity + bAcceleration * step / 2

      var cVelocity = tempVelocity
      var cAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity
      tempPosition = position + cVelocity * step / 2
      tempVelocity = velocity + cAcceleration * step / 2

      var dVelocity = tempVelocity
      var dAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity
      tempPosition = position + cVelocity * step / 2
      tempVelocity = velocity + cAcceleration * step / 2

      var dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6
      var dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6

      position += dxdt * step
      velocity += dvdt * step
    }

    this._lastTime = now
    this._lastPosition = position
    this._lastVelocity = velocity

    this._onUpdate(position)
    if (!this.__active) { // a listener might have stopped us in _onUpdate
      return
    }

    // Conditions for stopping the spring animation
    var isOvershooting = false
    if (this._overshootClamping && this._tension !== 0) {
      if (this._startPosition < this._toValue) {
        isOvershooting = position > this._toValue
      } else {
        isOvershooting = position < this._toValue
      }
    }
    var isVelocity = Math.abs(velocity) <= this._restSpeedThreshold
    var isDisplacement = true
    if (this._tension !== 0) {
      isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold
    }

    if (isOvershooting || (isVelocity && isDisplacement)) {
      if (this._tension !== 0) {
        // Ensure that we end up with a round value
        this._onUpdate(this._toValue)
      }

      this.__debouncedOnEnd({finished: true})
      return
    }
    this._animationFrame = RequestAnimationFrame.current(this.onUpdate.bind(this))
  }

  stop(): void {
    this.__active = false
    CancelAnimationFrame.current(this._animationFrame)
    this.__debouncedOnEnd({finished: false})
  }
}

module.exports = SpringAnimation
